# -*- mode: python; coding: utf-8; -*-
##--------------------------------------------------------------------------##
##
## Copyright (C) 1998-2003 Markus Oberhumer <markus@oberhumer.com>
## Copyright (C) 2003 Mt. Hood Playing Card Co.
## Copyright (C) 2005-2010 Skomoroh <skomoroh@users.sourceforge.net>
## Copyright (C) 2010-2013 Jeremy Austin-Bardo <tjaustinbardo@gmail.com>
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation, version 3 of the License.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program.  If not, see <http://www.gnu.org/licenses/>.
##
##--------------------------------------------------------------------------##
"""
Load card and base image with resizing.
"""
from os.path import join as jpath
import logging

LOGGER = logging.getLogger('PySolFC.Packlib.ImageSet')

from . import group as CSG
from ..utillib.misc import BACK, Size, Offset, SPACE, Scale, BASE
from ..viewlib.image import load_image, create_image


GREY = 'a0a0a0'
SHADE = '#3896f8'
WHITE = '#ffffff'
BLACK = '#000000'


class Images(object):
    """
    Images for Solitaire. Cache Card faces and backs for current cardset
    details. Cache base images for underneath card stacks.
    """

    def __init__(self, pack_detail):
        """
        Initialize image set with given pack detail.

        .. \__init__(cs:obj) -> None

        *Parameters:*
          ``pack_detail``: CardInfo
        """
        assert pack_detail is not None
        self.__detail = pack_detail
        self._scale = Scale(1.0, 1.0,)
        self.__back_menu = []
        self._card = []
        self._back = []
        self._base = []
        # Used when dragging cards
        self._shadow = {}           # key: (width, height)
        self.__flag = False
        self._shade = None

    @staticmethod
    def subsample(images, ratio=2):
        """
        Return new subsample image set.

        Create a new Images with given image set that given ratio smaller. Used
        by game preview to show cards on its reduced canvas inside a dialog.

        .. subsample(images:Images, ratio:int=2) -> Images

        *Parameters:*
          ``images``: {Images}
            Current image set.
          ``ratio``: {number} 2
            Divide current image size by given value.
        """
        s_image = Images(images.detail)
        s_image._card = images._card
        s_image._back = images._back
        s_image._base = images._base

        s_image.resize(1.0 / ratio, 1.0 / ratio)
        return s_image

    def __load_image(self, name, check=True, ext=False):
        """
        Return loaded image or None.

        Load given filename optionally checking dimensions match pack detials.

        .. \__load_image(name:str[, check=True:bool, ext=False:bool]) ->
           Image | None

        *Parameters:*
          ``name``: {text}
            Name of file to load and convert for UI toolkit.
          ``check``: {bool} True
            Check dimensions match pack detail size field.
          ``ext``: {bool} False
            Given name contains its file extension.
        """
        from ..utillib.pathdb import IMAGE_EXT_ITER
        from os.path import isfile

        path = jpath(self.detail['dir'], name)

        if not ext:
            for e_path in IMAGE_EXT_ITER(path):
                if isfile(e_path):
                    path = e_path
                    break
            else:
                LOGGER.warning('Invalid extension for %s.*', path)
                return

        LOGGER.info('adding image for path: %s', path)

        try:
            image_ = load_image(path)
        except:  # FIXME: need specific error when load and convert image.
            LOGGER.warning('Could not load and convert %s.*', path)
        else:
            if check and image_.size != self.detail['size']:
                LOGGER.warning('Invalid size for %s.*', path)
            else:
                return image_

    def __load_base(self, name, index, flag):
        """
        Return loaded base image.

        Load given name and index with given flag. Call load image method for
        basic base. When pack type has base symbol directory, compose new image
        with given flag of card/tile size.

        .. \__load_base(name:str, index, flag:bool) -> Image

        *Parameters:*
          ``name``: {text}
            Prefix name of file to load either 'bottom' or 'l'.
          ``index``: {number}
            Number of image to load.
          ``flag``: {bool}
            Quality... negative image '-n' or positive image ''.
        """
        from ..utillib.pathdb import PATH_DB
        name, color = [('{}{:02}'.format(name, index), BLACK,),
                       ('{}{:02}-n'.format(name, index), WHITE,)][int(flag)]

        # Find common base directory for pack type.
        d_image = PATH_DB.find_dir(CSG.SUBDIR[self.detail['type']],
                                    subdirs=jpath('images', 'cards', 'bottoms'))
        if not d_image:
            LOGGER.info('Fallback, using pack directory.', name)
            image = self.__load_image(name + self.detail['ext'], ext=True)
            return image.resize(*self.scale)

        # Find common base image for pack type.
        try:
            f_image = PATH_DB.find_image(name, subdirs=d_image)
        except OSError:
            LOGGER.info('Fallback, using blank for %s.', name)
            f_image = None

        # Composite new image bottom
        LOGGER.info('Composing new image with %s.', name)
        return self._card[0].new_bottom(f_image, color)

    def load(self, progress=None):
        """
        Load image set for pack detail.

        Load card face and backs. Load related bases and shade. Return success.

        .. load([progress:Progress=None]) -> bool

        *Parameters:*
          ``Progress``: {Progress}
            When progress, show progress.

        """
        assert (len(self.detail['suits']) * len(self.detail['ranks']) +
                len(self.detail['trumps'])) == self.detail['ncards']

        pstep = 0 if progress is None else max(0, (80.0 - progress.percent) /
                                                  (4 + self.detail['ncards']))

        def do_progress():
            """
            Update progress dialog when available.
            """
            if progress:
                progress.update(step=pstep)

        def image_name_iter():
            """
            Iterate image names from complete pack.
            """
            for suit in self.detail['suits']:
                for rank in self.detail['ranks']:
                    yield '{:>02}{}'.format(rank + 1, suit)  # int, chr
            for trump in self.detail['trumps']:
                yield '{:>02}{}'.format(trump + 1, 'z')  # int, chr
                do_progress()

        pack_name = self.detail['name']
        load = self.__load_image
        # load card faces
        self._card =  tuple(i for i in (load(n) for n in image_name_iter())
                            if i is not None)
        if len(self._card) != self.detail['ncards']:
            LOGGER.warning('Not enough cards added for %s.', pack_name)
            return False

        # load card backs.
        back_names = self.detail['backnames']
        self._back = tuple(BACK(name=n, image=i) for n, i in
                          ([n, load(n, ext=True)] for n in back_names)
                          if i is not None)
        if len(self._back) != len(back_names):
            LOGGER.warning('Not enough backs added for %s.', pack_name)
            return False
        if not self._back:
            self._back = (BACK(name='',
                     image=create_image(self.size, fill=GREY, outline=BLACK)),)
            back_names += ('',)
        do_progress()

        # load bases
        comp = lambda n, i: BASE(self.__load_base(n, i, False),
                                 self.__load_base(n, i, True))
        # 1st half bottom add increases. (+ index values)
        nbottoms = self.detail['nbottoms']
        bases = [comp('bottom', i) for i in range(1, nbottoms + 1)]
        if len(bases) != nbottoms:
            LOGGER.warning('Not enough bottoms added for %s.', pack_name)
            return False
        do_progress()
        # 2nd half bottom add decreases. (- index values)
        nletters = self.detail['nletters']
        bases += [comp('l', i) for i in range(nletters, 0, -1)]
        if len(bases[nbottoms:]) !=  nletters:
            LOGGER.warning('Not enough letters added for %s.', pack_name)
            return False
        do_progress()
        self._base = tuple(bases)
        self._shade = self._card[0]._shade_image
        return True

    @property
    def back(self):
        """
        Return back image for pack detail.

        Get back by current *backindex* field of back images loaded.

        *note:*
          Highlight function is now ``_shade`` method on Card itself.

        .. back -> Image
        """
        index = self.detail['backindex'] % len(self._back)
        LOGGER.info('get back: %s, [%d]', self._back[index].name, index)
        return self._back[index].image

    def face(self, card):
        """
        Return face image for pack detail.

        Get face by given card suit and rank attribute from face images loaded.

        *note:*
          Highlight function is now ``_shade`` method on Card itself.

        .. face(card:Card) -> object

        *Parameters:*
          ``card``: {Card}
            Use suit and rank attributes to get its image.
        """
        pdi = self.detail
        index = card['suit'] * len(pdi['ranks']) + card['rank'] % pdi['ncards']
        LOGGER.info('get suit: %d, rank: %d, [%d]', card['suit'], card['rank'],
                                                    index)
        return self._card[index]

    def bottom(self, mode, index=-1):
        """
        Return bottom image for pack detail.

        Get bottom by given mode and index from bottom images loaded. Shown
        under piles to provide location and indicate purpose with current light.

          - ``shade`` indicates empty pile that usually still build.
          - ``braid`` indicates empty *braid* pile.
          - ``suit`` indicates empty pile that must start build with suit.
          - ``rank`` indicates empty pile that must start build with rank.
          - ``blank`` indicates empty pile that usually does not still build.

        .. bottom(mode:str, index:int=-1]) -> Image | None

        *Parameters:*
          ``mode`` : {string}
            Value of 1 of 5 types indicated. Used to help get bottom image.
          ``index``: {number} -1
            Value of available rank or suit. Default is any value for suit
            or ACE.

        *Raise:*
          ``AssertionError``:
            When given index is not number.
        """
        assert isinstance(index, int)

        mode = mode.lower()

        if mode == 'shade':
            return self._base[0][int(self.__flag)]
        elif mode == 'braid':
            return self._base[2][int(self.__flag)]
        elif mode == 'suit':
            # any index out of range is converted to shade bottom.
            check, index = divmod({-1: 1}.get(index, index + 3),
                                  self.detail['nbottoms'])
            return self._base[0 if check else index][int(self.__flag)]
        elif mode == 'rank':
            # any index out of range is converted to shade bottom
            check, index = divmod({-1: 0}.get(index, index),
                                   self.detail['nletters'])
            return self._base[0 if check else -index + 1][int(self.__flag)]
        elif mode == 'blank':
            return create_image(self.size, fill=None)

#    def shadow(self, stack, cards):
#        """
#        Return shadow image for pack detail.

#        Get shadow for under given cards. Use given stack to get each current
#        position with CompositeCards class.

#        .. shadow(stack:obj, cards:obj) -> Image

#        *Parameters:*
#          ``stack``: {Pile}

#          ``cards``: {seq(Card ...)}
#            Sequence to be dragged.
#        """
#        comp_cards = CompositeCards(stack, cards)
#        if comp_cards.size in self._shadow:
#            return self._shadow[comp_cards.size]
#       shadow = comp_cards.shadow(self.shadow_offset)
#        self._shadow[comp_cards.size] = shadow
#        return shadow

    @property
    def random_face(self):
        """
        .. random_face -> Image
        """
        from random import choice
        return choice(self._card)

    @property
    def sample_faces_iter(self):
        """
        Iterate samples of card/tile from a complete set of pack.

        Yield first card and last three cards of each suit. Yield first 4 cards
        of trumps.

        .. sample_faces_iter ~> iter(seq{int, int} ...)
        """
        ncards = len(self.detail['suits']) * len(self.detail['ranks'])

        nranks = len(self.detail['ranks'])
        for index in range(0, ncards, nranks):
            r_set = self._card[index:index + nranks]
            for card in [r_set[0]] + r_set[-3:]:
                yield card

        ntrumps = len(self.detail['trumps'])
        if ntrumps > 0:
            for card in self._card[-ntrumps:4 - ntrumps]:
                yield card

    def bottom_light(self, flag=False):
        """
        Set base image light.

        .. bottom_light([flag:bool=False]) -> Image

        *Parameters:*
          ``flag``: {bool}
            When True normal image of symbol on darker base, else negative image
            of symbol on lighter base.
        """
        self.__flag = bool(flag)

    @property
    def highlight(self):
        """
        Return highlight image for current pack detail.

        .. highlight -> Image

        :return: current image highlight.
        """
        return self._shade

    @property
    def detail(self):
        """
        Return pack detail.

        .. detail -> CardInfo
        """
        return self.__detail

    @property
    def scale(self):
        """
        Return image set scale.

        .. scale -> Scale(float, float)
        """
        return Scale(*[float(factor) for factor in self._scale])

    @property
    def size(self):
        """
        Return scaled size field of pack detail.

        .. size -> Size(int, int)
        """
        scale = self.scale
        return Size(int(self.__detail['size'].width * scale.xscale),
                    int(self.__detail['size'].height * scale.yscale))

    @property
    def offset(self):
        """
        Return scaled offset field of pack detail.

        .. offset -> Offset(int, int)
        """
        scale = self.scale
        offset = Offset(int(self.__detail['offset'].xoffset * scale.xscale),
                        int(self.__detail['offset'].yoffset * scale.yscale))
        if any(i < 1 for i in scale):
            offset = Offset(max(10 * scale.xscale, offset.xoffset),
                            max(10 * scale.yscale, offset.yoffset))
        return offset

    @property
    def space(self):
        """
        Return scaled image set size with space.

        Space returned will 110% of image width and and 115% of image height.
        Additional space will not be less than 6px added to width and 10px to
        height .

        .. space -> SPACE(int, int)
        """
        size = self.size
        return SPACE(int(size.width + min(6, int(size.width * .10))),
                     int(size.height + min(10, int(size.width * .15))))

    @property
    def shadow_offset(self):
        """
        Return scaled shadow edge size for image set.

        .. shadow_offset -> Offset(int, int).
        """
        scale = self.scale
        return Offset(int(self.__detail['shadow'].xoffset * scale.xscale),
                      int(self.__detail['shadow'].yoffset * scale.yscale))

    def resize(self, xscale, yscale):
        """
        Resize image set.

        .. resize(xscale:float, yscale:float) -> None
        """
        LOGGER.info('Card scale: %d:%d', xscale, yscale)
        scale = Scale(xscale, yscale)
        if self.scale == scale:
            LOGGER.info('no resize')
            return
        LOGGER.info('Original card size: (%d, %d)', *self.size)
        self._scale = scale
        LOGGER.info('Factor card size: (%d, %d)', *self.size)
        # cards
        self._card = tuple(c.resize(*scale) for c in self._card)
        # back
        self._back = tuple(BACK(b.name, b.image.resize(*scale))
                           for b in self._back)
        # base
        self._base = tuple(BASE(b.negative.resize(*scale),
                                 b.positive.resize(*scale))
                            for b in self._base)
        self._shadow = {}
        self._shade = self._card[0]._shade_image

    def reset(self):
        """
        Reset image set scale to 1.

        .. reset() -> None
        """
        LOGGER.info('Image.reset')
        self.resize(1.0, 1.0)